//-----------------------------------------------------------------------------
//  Copyright (C) 2005-2008 Thomas S. Ullrich 
//
//  This file is part of the Solitaire Mahjong project.
//
//  This file may be used under the terms of the GNU General Public License.
//  This project is free software; you can redistribute it and/or modify it
//  under the terms of the GNU General Public License.
//  
//  Author: Thomas S. Ullrich
//  Last update: November 14, 2008
//-----------------------------------------------------------------------------
#include <iostream>
#include <algorithm>
#include <numeric>
#include <fstream>
#include <ctime>
#include <cmath>
#include <QStatusBar>
#include <QMessageBox>
#include <QInputDialog>
#include <QProgressDialog>
#include <QFileDialog>
#include <QtDebug>
#include "smMainWindow.h"
#include "smSettings.h"
#include "smVersion.h"

using namespace std;

smMainWindow::smMainWindow()
{
    // setAttribute(Qt::WA_DeleteOnClose);
    setAttribute(Qt::WA_QuitOnClose);
    
    //
    //  Init data member
    //
    mTileFactory = 0;
    mTileLayout = 0;
    mGameIsOn = false;
    mShowMessageAtEndOfGame = true;
    mFirstSelected = 0;
    mSecondSelected = 0;
    mCurrentNumberOfTiles = 0;
    mCurrentNumberOfUndos = 0;
    mCurrentNumberOfHints = 0;    
    
    //
    //  Get setting/preferences
    //
    mHighscoreList = new smHighscoreList(this);
    smSettings::instance()->setHighscoreList(mHighscoreList); // needs to be done before load()
    smSettings::instance()->load();

    //
    //  Setup main window and all widgets within it
    //
    ui.setupUi(this);
    mBoardView = new smBoardView(this);
    setCentralWidget(mBoardView);
    mBoardView->scene()->setBackgroundBrush(Qt::darkGreen);
    createStatusBar();
	
	//
	//  Setup all remaining dialogs
	//
    mPreferencesDialog = new smPreferencesDialog(this); // requires settings to be loaded
	mAboutDialog = new smAboutDialog(this);	
	mPlayedTilesList = new smPlayedTilesList(this);	

    //
	// Connect signals and slots
	//
	connect(ui.actionPreferences, SIGNAL(activated()), this, SLOT(showPreferences()));
	connect(ui.actionAbout_Solitaire_Mahjong, SIGNAL(activated()), this, SLOT(showAboutWindow()));
	connect(ui.actionContents, SIGNAL(activated()), this, SLOT(showHelpBrowser()));
	connect(ui.actionNew_Game, SIGNAL(activated()), this, SLOT(newGame()));
	connect(ui.actionOpen, SIGNAL(activated()), this, SLOT(newGameFromFile()));
	connect(ui.actionSave, SIGNAL(activated()), this, SLOT(saveGameToFile()));
	connect(ui.actionHighscores, SIGNAL(activated()), this, SLOT(showHighscoreList()));
	connect(ui.actionPlayedTiles, SIGNAL(activated()), this, SLOT(showPlayedTiles()));
	connect(ui.actionHint, SIGNAL(activated()), this, SLOT(hintNextMove()));
	connect(ui.actionReplay, SIGNAL(activated()), this, SLOT(replay()));
	connect(ui.actionUndo, SIGNAL(activated()), this, SLOT(undoMove()));
    connect(ui.actionFinish_Game, SIGNAL(activated()), this, SLOT(computerVersusComputer()));
	connect(ui.actionFind_Solution, SIGNAL(activated()), this, SLOT(searchSolution()));
	connect(ui.actionAnalyze_Layout, SIGNAL(activated()), this, SLOT(analyzeLayout()));
    connect(ui.actionExit, SIGNAL(triggered()), qApp, SLOT(closeAllWindows()));
    
    //
    // Status bar
    //
    statusBar()->clearMessage();
    mMessageLabel->setText("Press Ctrl+N or select 'New Game' in the 'Game' menu to start a new game.");
    
    move(smSettings::instance()->mMainWindowPosition);
    
    newGame();
};

smMainWindow::~smMainWindow()
{
    if (mGameIsOn) {
        delete mTileFactory; mTileFactory = 0;
        delete mTileLayout;  mTileLayout = 0;
    }
    delete mPreferencesDialog; mPreferencesDialog = 0;
    delete mPlayedTilesList; mPlayedTilesList = 0;
    smSettings::instance()->mMainWindowPosition = pos();
    smSettings::instance()->save();
    delete mHighscoreList; mHighscoreList = 0;
}

void smMainWindow::analyzeLayout()
{
    if (QMessageBox::information(this, "Analyze Current Layout",
                                 "You are going to perform a deep analysis of the current layout.\n\n"
                                 "A process will be started that analyzes several games using\n"
                                 "the current layout. For each game the algorithm will try to find \n"
                                 "the best solution, that is the solution that removes the most tiles.\n\n"
                                 "The results are written to a file 'analysis.out'. You will be\n"
                                 "prompted for the number of games to analyze and the maximum\n"
                                 "number of iteration allowed to find the solution for each game.\n\n"
                                 "Note that this process can take several hours depending on the\n"
                                 "specified parameters.", QMessageBox::Ok, QMessageBox::Cancel)
        == QMessageBox::Cancel) return;
    
    bool ok;
    int maxLayouts = QInputDialog::getInteger(this, "Analyze Current Layout",
                                              "Enter number of games to play:",
                                              100, 10, 10000, 10, &ok);
    if (!ok) return;
    int maxIterations = QInputDialog::getInteger(this, "Analyze Current Layout",
                                                 "Enter number of iterations to perform:",
                                                 100, 10, 10000, 10, &ok);
    if (!ok) return;
    
    QProgressDialog progress( "Analyzing current layout ...", "Cancel", 0, maxLayouts,
                             this);
    
    mShowMessageAtEndOfGame = false;
    bool hintModeStatus = smSettings::instance()->mRandomHint;
    smSettings::instance()->mRandomHint = true;
    smTileItem::setUpdateEnabled(false);
    
    int best;
    int i, j;
    vector<int> histogram(smMaxNumberOfTiles, 0);
    vector<int> iterations(smMaxNumberOfTiles, 0);
    progress.setValue(0);
    for (i=0; i<maxLayouts; i++) {
        newGame();
        best = smMaxNumberOfTiles;
        for (j=0; j<maxIterations; j++) {
            if (best == 0) break;
            replay();
            computerVersusComputer();
            progress.setValue(i);
            qApp->processEvents();
            if (progress.wasCanceled())
                break;
            if (mCurrentNumberOfTiles < best)
                best = mCurrentNumberOfTiles;
        }
        if (progress.wasCanceled()) break;
        histogram[best] = histogram[best]+1;
        iterations[best] = iterations[best]+j;
    }
    
    progress.setValue(maxLayouts);
    
    mShowMessageAtEndOfGame = true;
    smSettings::instance()->mRandomHint = hintModeStatus;
    smTileItem::setUpdateEnabled(true);
    replay();
    
    double sum = accumulate(histogram.begin(), histogram.end(), 0);
    double mean = 0;
    double mean2 = 0;
    double xw;
    unsigned int k;
    for (k=0; k<histogram.size(); k++) {
        xw = k*histogram[k];
        mean += xw;
        mean2 += xw*xw;
    }
    mean /= sum;
    mean2 /= sum;
    double sigma = sqrt(mean2-mean*mean);
    
    ofstream ofs("analysis.out");
    time_t now = time(0);
    ofs << ctime(&now);
    ofs << "Games: " << i << endl;
    ofs << "Maximum iterations: " << maxIterations << endl;
    ofs << "======= Results =======" << endl;
    ofs << "sum: " << sum << endl;
    ofs << "mean: " << mean << endl;
    ofs << "sigma: " << sigma << endl;
    ofs << "tiles left\t frequency\t probability\t <iterations>" << endl;
    for (k=0; k<histogram.size(); k++) {
        ofs << k << '\t' << histogram[k] << '\t' << histogram[k]/sum << '\t';
        if (histogram[k])
            ofs << static_cast<double>(iterations[k])/histogram[k] << endl;
        else
            ofs << '-' << endl;
    }
    char text[128];
    sprintf(text,"Finished analysis (see 'analysis.out'). "
            "Analyzed %d games, each with maximal %d iterations.", i, maxIterations);
    statusBar()->clearMessage();
    mMessageLabel->setText(text);
}

void smMainWindow::computerVersusComputer()
{
    statusBar()->clearMessage();
    while (mGameIsOn) {
        hintNextMove();
        itemsSelectedForRemoval();
        mMessageLabel->setText("Computer versus computer ...");
        update();
        qApp->processEvents();
    }
}

void smMainWindow::createStatusBar()
{
    mTileCountLabel = new QLabel("0", this);
    mTileCountLabel->setAlignment(Qt::AlignHCenter);
    mTileCountLabel->setMinimumWidth(statusBar()->fontMetrics().width("144"));
    mTileCountLabel->setFrameStyle(QFrame::Panel | QFrame::Sunken);    
    
    mMessageLabel = new QLabel(this);
    mMessageLabel->setAlignment(Qt::AlignLeft);
    mMessageLabel->setIndent(4);
    mMessageLabel->setFrameStyle(QFrame::Panel | QFrame::Sunken);    
    
    mUndoHintCountLabel = new QLabel("0/0", this);
    mUndoHintCountLabel->setAlignment(Qt::AlignHCenter);
    mUndoHintCountLabel->setMinimumSize(mUndoHintCountLabel->sizeHint());
    mUndoHintCountLabel->setMinimumWidth(statusBar()->fontMetrics().width("72/72"));
    mUndoHintCountLabel->setFrameStyle(QFrame::Panel | QFrame::Sunken);    
    
    statusBar()->addWidget(mTileCountLabel, 0);
    statusBar()->addWidget(mMessageLabel, 1);
    statusBar()->addWidget(mUndoHintCountLabel, 0);
    statusBar()->setSizeGripEnabled(false);
}

void smMainWindow::endOfGame()
{
    mEndTime = time(0);
    
    //
    //  Handle highscore (if at all)
    //
    
    smHighscoreEntry highscore(mStartTime, mEndTime, mCurrentNumberOfTiles,
                               mCurrentNumberOfHints, mCurrentNumberOfUndos);
    bool isInHighscoreList = mHighscoreList->insertEntry(highscore);
    
    //
    //  Print the message and popup window.
    //  If the current game makes it into the high score
    //  list, high score window is shown once the user
    //  closes the message dialog.
    //
    char text[256];
    if (mCurrentNumberOfTiles) {             // user lost
        sprintf(text, "No more moves. %d tiles (%d%%) left.",
                mCurrentNumberOfTiles, mCurrentNumberOfTiles*100/smMaxNumberOfTiles);
        mMessageLabel->setText(text);
        if (isInHighscoreList)
            sprintf(text, "%s\nThis game will enter the high score list.", text);
        if (mShowMessageAtEndOfGame)
            QMessageBox::information( this, "Solitaire Mahjong", text, QMessageBox::Ok);
    }
    else {                                   // user won
        sprintf(text, "Congratulations! All %d tiles removed.", smMaxNumberOfTiles);
        mMessageLabel->setText(text);
        mMessageLabel->repaint();
        time_t deltaTime =  mEndTime-mStartTime+1;
        time_t deltaMin = deltaTime/60;
        time_t deltaSec = deltaTime-deltaMin*60;
        sprintf(text, "Congratulations! All %d tiles removed.\n"
                "You got %d hints and performed %d undos.\n"
                "Duration of game was %d min and %d sec.",
                smMaxNumberOfTiles, mCurrentNumberOfHints, mCurrentNumberOfUndos, 
                static_cast<unsigned int>(deltaMin), static_cast<unsigned int>(deltaSec));
        if (isInHighscoreList)
            sprintf(text, "%s\nThis game will enter the high score list.", text);
        if (mShowMessageAtEndOfGame)
            QMessageBox::information( this, "Solitaire Mahjong", text, QMessageBox::Ok);   
    }
    if (mShowMessageAtEndOfGame && isInHighscoreList) mHighscoreList->syncAndShow();
    
    //
    //  Set game flag and disable commands (actions) that
    //  are not applicable once the game ended.
    //
    mGameIsOn = false;
    ui.actionUndo->setEnabled(false);
    ui.actionHint->setEnabled(false);
    ui.actionReplay->setEnabled(true);
    ui.actionFinish_Game->setEnabled(false);
}

pair<smTileItem*, smTileItem*> smMainWindow::hint(int* npossible) const
{
    //
    //  Select tiles that are visible and free
    //  to speed up the loop.
    //
    smTileItem *item;
    vector<smTileItem*> usable;
    for (unsigned int k=0; k<mTileFactory->maxNumberOfTiles(); k++) {
        item = mTileFactory->tile(k);
        if (item->isVisible() && mTileLayout->isAtBorder(item)) usable.push_back(item);
    }
    
    //
    //  Check all combinations, and store the pairs
    //  in a vector, that at the end will contain
    //  all possible moves.
    //
    if (npossible) *npossible = 0;
    vector<pair<smTileItem*, smTileItem*> > theHints;
    smTileItem *item1, *item2;
    for (unsigned int i=0; i<usable.size(); i++) {
        item1 = usable[i];
        for (unsigned int j=i+1; j<usable.size(); j++) {
            item2 = usable[j];
            if (itemsMatch(item1, item2)) {
                if (npossible) (*npossible)++;
                theHints.push_back(pair<smTileItem*, smTileItem*>(item1, item2));
            }
        }
    }
    if (theHints.size()) {
        if (smSettings::instance()->mRandomHint)
            return theHints[rand()%theHints.size()];
        else
            return theHints[0];
    }
    return pair<smTileItem*, smTileItem*>(0, 0);
}

void smMainWindow::hintNextMove()
{
#if defined(Q_OS_MACX)
    setFocus();   // get focus back from the Mac top menu bar
#endif
    int nmoves;
    pair<smTileItem*, smTileItem*> p = hint(&nmoves);
    if (!p.first) return;
    if (mFirstSelected)
        mFirstSelected->setHighlighted(false);
    mFirstSelected = p.first;
    mFirstSelected->setHighlighted(true);
    
    if (mSecondSelected)
        mSecondSelected->setHighlighted(false);
    mSecondSelected = p.second;
    mSecondSelected->setHighlighted(true);
    
    mCurrentNumberOfHints++;
    
    char text[64];
    statusBar()->clearMessage();  // get rid of temporary messages
    sprintf(text, " %d/%d ", mCurrentNumberOfHints, mCurrentNumberOfUndos);
    mUndoHintCountLabel->setText(text);
    mUndoHintCountLabel->repaint();
    
    //
    // Optional: tell how many possible moves there are left
    //
    if (smSettings::instance()->mShowNumberOfPossibleMoves) {
        sprintf(text, "Shown match is one of %d possible matches", nmoves);
        mMessageLabel->setText(text);
    }
}

void smMainWindow::itemSelected(smTileItem* item)  // called on left mouse click
{
    statusBar()->clearMessage(); // remove temporary messages from menu bars etc.
    if (mGameIsOn && item) {
        if (mTileLayout->isAtBorder(item)) {
            if (item->highlighted()) {
                item->setHighlighted(false);
                removeSelectedItem(item);
            }
            else {
                if (smSettings::instance()->mShowTileNamesInStatusBar) {
                    mMessageLabel->setText(smDisplayTileName[item->id()]);
                }
                else
                    mMessageLabel->setText("");
                if (numberOfItemsSelected() == 0) {
                    item->setHighlighted(true);
                    addSelectedItem(item);
                }
                else if (numberOfItemsSelected() == 1) {
                    if (itemsMatch(mFirstSelected, item)) {
                        item->setHighlighted(true);
                        addSelectedItem(item);
                    }
                    else {
                        mFirstSelected->setHighlighted(false);
                        removeSelectedItem(mFirstSelected);
                        item->setHighlighted(true);
                        addSelectedItem(item);
                    }
                }
            }
        }
        else {
            if (smSettings::instance()->mBeepWhenTileNotFree)
                qApp->beep();
        }
    }
    return;
}

void smMainWindow::itemsSelectedForRemoval()  // called on right mouse click
{
    statusBar()->clearMessage(); // remove temporary messages from menu bars etc.
    
    char text[256];
    if (!mGameIsOn) return;
    if (numberOfItemsSelected() == 2 && selectedItemsMatch()) {
        mHistoryStack.push(pair<smTileItem*, smTileItem*>(mFirstSelected, mSecondSelected));
        mFirstSelected->remove();
        mTileLayout->removeTile(mFirstSelected);
        mSecondSelected->remove();
        mTileLayout->removeTile(mSecondSelected);
        mFirstSelected = mSecondSelected = 0;
        mCurrentNumberOfTiles -= 2;
    }
    sprintf(text, " %d ",mCurrentNumberOfTiles);
    mTileCountLabel->setText(text);
    mMessageLabel->setText("");
    mTileCountLabel->repaint();
    
    if (mHistoryStack.empty()) {
        ui.actionUndo->setEnabled(false);
        ui.actionReplay->setEnabled(false);
    }
    else {
        ui.actionUndo->setEnabled(true);
        ui.actionReplay->setEnabled(true);
    }
    
    pair<smTileItem*, smTileItem*> p = hint();
    if (!p.first) endOfGame();
}

bool smMainWindow::loadGameFromFile(multimap<int, int>& tileMap)
{
    tileMap.clear();
    
    //
    // Read tile (or better their order) from file.
    // The actual order (index <-> id) is returned in a
    // multimap.
    //
    char str[256];
    
    QString filename = QFileDialog::getOpenFileName(this, "Load Game", smSettings::instance()->mLastVisitedDirectory,
                                                    "Solitaire Mahjong File (*.ssm)");
    
    if (filename.isEmpty()) return false;
    
    QString currentDir = filename;
    currentDir.truncate(currentDir.lastIndexOf('/'));
    smSettings::instance()->mLastVisitedDirectory = currentDir;
    
    ifstream ifs(filename.toAscii());
    if (!ifs) {
        sprintf(str,"Failed to open file '%s'.\nCannot load game.\n", static_cast<const char*>(filename.toAscii()));
        QMessageBox::warning(0, "Solitaire Mahjong", str);
        return false;
    }
    
    string line;
    int ntiles, aIndex, aId, aColumn, aRow, aLayer;
    getline(ifs, line); // header line
    getline(ifs, line); // date
    getline(ifs, line); // version, ignore for now
    getline(ifs, line); // tile set and layout, ignore for now, there's only one of each
    ifs >> ntiles;       // number of tiles
    if (ntiles != smMaxNumberOfTiles) {
        sprintf(str,"File '%s'\ndoes not contain the required number of tiles.\nCannot load game.\n", static_cast<const char*>(filename.toAscii()));
        QMessageBox::warning(0, "Solitaire Mahjong", str);
        return false;
    }
    
    typedef pair<int, int> mapEntry;
    for (int i=0; i<ntiles; i++) {
        aIndex = aId = aColumn = aRow = aLayer = -1;
        ifs >> aIndex >> aId >> aColumn >> aRow >> aLayer;
        if (ifs.eof() || ifs.bad()) {
            sprintf(str,"File '%s':\nreached end-of-file before loading all tiles.\nCannot load game.\n", static_cast<const char*>(filename.toAscii()));
            QMessageBox::warning(0, "Solitaire Mahjong", str);
            return false;
        }
        if (aId >= 0 && aId < NumberOfTiles && aIndex >= 0 && aIndex < smMaxNumberOfTiles)
            tileMap.insert(mapEntry(aId, aIndex));
    }
    getline(ifs, line);  // read rest of line
    getline(ifs, line);  // get #EOF tag
    if (line != "#EOF") {
        sprintf(str,"File '%s':\nmissing internal end-of-file tag.\nFile probably damaged. Cannot load game.\n", static_cast<const char*>(filename.toAscii()));
        QMessageBox::warning(0, "Solitaire Mahjong", str);
        return false;
    }
    
    ifs.close();
    
    if (static_cast<int>(tileMap.size()) != smMaxNumberOfTiles) {
        sprintf(str,"File '%s'\ndoes not contain the required number of tiles.\nCannot load game.\n", static_cast<const char*>(filename.toAscii()));
        QMessageBox::warning(0, "Solitaire Mahjong", str);
        return false;
    }
    
    return true;
}

void smMainWindow::newGame()
{
    startGame(false);
}

void smMainWindow::newGameFromFile()
{
    startGame(true);
}

void smMainWindow::replay()
{
    //
    //   Undo all moves. Use the single undoMove()
    //   here. It's quick enough and better than
    //   rewriting the code. Looks like a super
    //   fast rewind.
    //
    mGameIsOn = true;
    while (mCurrentNumberOfTiles < smMaxNumberOfTiles) undoMove();
    
    //
    //   Reset all counters. Enable/disable actions.
    //
    mCurrentNumberOfTiles = smMaxNumberOfTiles;
    mCurrentNumberOfUndos = 0;
    mCurrentNumberOfHints = 0;
    ui.actionHint->setEnabled(true);
    ui.actionUndo->setEnabled(false);
    ui.actionReplay->setEnabled(false);
    ui.actionFinish_Game->setEnabled(true);
    
    //
    //   Messages for status bar
    //
    char text[100];
    statusBar()->clearMessage(); // remove temporary messages from menu bars etc.
    sprintf(text, "Previous setup restored (%d tiles). Ready to play.", mCurrentNumberOfTiles);
    mMessageLabel->setText(text);
    sprintf(text, " %d ",mCurrentNumberOfTiles);
    mTileCountLabel->setText(text);
    sprintf(text, " %d/%d ", mCurrentNumberOfHints, mCurrentNumberOfUndos);
    mUndoHintCountLabel->setText(text);
#if defined(Q_OS_MACX)
    setFocus();  // get focus back from the Mac top menu bar
#endif
    mStartTime = time(0);
}

void smMainWindow::saveGameToFile()
{
    char str[256];
    
    QString defaultName = smSettings::instance()->mLastVisitedDirectory + "/mygame.ssm";
    QString filename = QFileDialog::getSaveFileName(this, "Save Game", defaultName, "Solitaire Mahjong File (*.ssm)");
    if (filename.isEmpty())
        return;
    else {
        if (!filename.endsWith(".ssm")) filename += ".ssm"; // enforce extension
        QString currentDir = filename;
        currentDir.truncate(currentDir.lastIndexOf('/'));
        smSettings::instance()->mLastVisitedDirectory = currentDir;
        
        QFile saveFile(filename);
        if (saveFile.exists()) {
            QString text = filename + " already exists.\nDo you want to replace it?";
            if (QMessageBox::warning(0, "Solitaire Mahjong", text, "Yes", "No")) return;
        }
        
        ofstream ofs(filename.toAscii());
        if (!ofs) {
            sprintf(str,"Cannot open/create file '%s'.\n Nothing stored.", static_cast<const char*>(filename.toAscii()));
            QMessageBox::information(0, "Solitaire Mahjong", str);
            return;
        }
        
        //
        // Write out the game.
        // The format is rather simple for now. All tiles as they
        // were layout at the start of the game.On the long term
        // one might add the actual tiles removed at that point,
        // including the hints etc.
        //
        
        vector<smTileItem*>& theTiles = mTileFactory->tiles();
        vector<smTileItem*>::iterator i;
        
        // Header
        ofs << "# Generated by Solitaire Mahjong, Version " << VERSION << endl;
        // Time
        time_t now = time(0);
        ofs << ctime(&now);
        // Version
        ofs << VERSION << endl;
        // Tile set and layout
        ofs << classic << '\t' << turtle << endl;
        // Number of tiles
        ofs << static_cast<unsigned int>(theTiles.size()) << endl;
        // Tiles as delivered by factory (recall they are already randomized)
        for (i=theTiles.begin(); i<theTiles.end(); i++) {
            ofs << (*i)->index()  << '\t'
            << (*i)->id()     << '\t'
            << (*i)->column() << '\t'
            << (*i)->row()    << '\t'
            << (*i)->layer()  << endl;
        }
        ofs << "#EOF" << endl;
        ofs.close();
    }
    
    sprintf(str,"Game saved in file '%s'", static_cast<const char*>(filename.toAscii()));
    statusBar()->clearMessage();
    mMessageLabel->setText(str);
}

void smMainWindow::saveSolution(stack<pair<smTileItem*, smTileItem*> > &historyStack)
{
    QString defaultName = smSettings::instance()->mLastVisitedDirectory + "/mysolution.txt";
    QString filename = QFileDialog::getSaveFileName(this, "Save File", defaultName, "Text File (*.txt)");
    if (filename.isEmpty()) return;
    
    if (!filename.endsWith(".txt")) filename += ".txt"; // enforce extension
    QString currentDir = filename;
    currentDir.truncate(currentDir.lastIndexOf('/'));
    smSettings::instance()->mLastVisitedDirectory = currentDir;
    
    QFile saveFile(filename);
    if (saveFile.exists()) {
        QString text = filename + " already exists.\nDo you want to replace it?";
        if (QMessageBox::warning(0, "Solitaire Mahjong", text, "Yes", "No")) return;
    }
    
    ofstream ofs(filename.toAscii());
    if (!ofs) {
        QString msg = "Cannot open/create file '" + filename + "'.\n" + "Solution not saved.";
        QMessageBox::information(0, "Solitaire Mahjong", msg);
        return;
    }
    
    time_t now = time(0);
    ofs << "# Generated by Solitaire Mahjong, Version " << VERSION << endl;
    ofs << "# " << ctime(&now);
    ofs << "# Format: " << endl;
    ofs << "# move-#: first-tile-name/row/column/layer | second-tile-name/row/column/layer" << endl;
    vector<string> game;
    char str[256];
    while(!historyStack.empty()) {
        pair<smTileItem*, smTileItem*> p = historyStack.top();
        historyStack.pop();
        sprintf(str,"%s/%d/%d/%d | %s/%d/%d/%d",
                smTileName[p.first->id()],
                p.first->row(),
                p.first->column(),
                p.first->layer(),
                smTileName[p.second->id()],
                p.second->row(),
                p.second->column(),
                p.second->layer());
        game.push_back(str);
    }
    for (unsigned int i=0; i<game.size(); i++)
        ofs << i+1 << ":\t" << game[game.size()-(i+1)] << endl;
    ofs.close();
}

void smMainWindow::searchSolution()
{
    //
    // Prompt for the number of iterations to perform
    //
    bool ok;
    int maxIterations = QInputDialog::getInteger(this, 
                                                 "Search for best solution",
                                                 "Enter number of iterations to perform:",
                                                 100, 10, 10000, 10, &ok);
    if (!ok)
        return;
    
    QProgressDialog progress( "Searching solution ...", "Cancel", 0, maxIterations,
                             this);
    //
    // mRandomHit must be on in order for this to work.
    // We put it back later to its current value.
    // We also switch off the modal dialog at the end
    // of each game otherwise the user has to constantly
    // click it away.
    //
    mShowMessageAtEndOfGame = false;
    bool hintModeStatus = smSettings::instance()->mRandomHint;
    smSettings::instance()->mRandomHint = true;
    
    //
    // Loop and search until we reach the max number
    // of requested iterations or we find a solution.
    //
    int best = smMaxNumberOfTiles;
    int iter;
    progress.setValue(0);
    for (iter=0; iter<maxIterations; iter++) {
        if (best == 0) break;
        replay();
        computerVersusComputer();
        update();
        progress.setValue(iter);
        qApp->processEvents();
        if (progress.wasCanceled())
            break;
        if (mCurrentNumberOfTiles < best)
            best = mCurrentNumberOfTiles;
    }
    progress.setValue(maxIterations);
    
    //
    // Reset control parameters to old status.
    // Keep copy of history stack in case the player wants
    // to save the solution (replay() clears the stack).
    //
    stack<pair<smTileItem*, smTileItem*> > historyStackCopy = mHistoryStack;
    mShowMessageAtEndOfGame = true;
    smSettings::instance()->mRandomHint = hintModeStatus;
    replay();
    
    //
    // Print what we got
    //
    char text[128];
    if (best)
        sprintf(text,"Best solution after %d iterations: %d tiles left", iter, best);
    else
        sprintf(text,"This layout is solvable. Found solution after %d iterations.", iter);
    
    statusBar()->clearMessage();
    mMessageLabel->setText(text);
    
    if (!best && QMessageBox::warning(0, "Solitaire Mahjong", "This layout is solvable.\nDo you want to store the solution?",
                                      "Yes", "No") == 0) saveSolution(historyStackCopy);
}

void smMainWindow::showAboutWindow()
{
    mAboutDialog->show();
}

void smMainWindow::showHelpBrowser()
{
//    
    smHelpBrowser::showPage("index.html");
}

void smMainWindow::showHighscoreList()
{
    mHighscoreList->move(smSettings::instance()->mHighscoreWindowPosition);
    mHighscoreList->syncAndShow();
}

void smMainWindow::showPlayedTiles()
{
    mPlayedTilesList->sync(mHistoryStack);
    mPlayedTilesList->move(smSettings::instance()->mPlayedTilesWindowPosition);
    mPlayedTilesList->show();
}

void smMainWindow::showPreferences()
{
    mPreferencesDialog->move(smSettings::instance()->mPreferencesWindowPosition);
    mPreferencesDialog->show();
}

void smMainWindow::startGame(bool useTilesFromFile)
{
    //
    //  Background color of board
    //
    
    mBoardView->scene()->setBackgroundBrush(smSettings::instance()->mBackgroundColor);
    
    //
    //  Clear history stack and zero old selections
    //
    while (!mHistoryStack.empty()) mHistoryStack.pop();
    mFirstSelected = 0;
    mSecondSelected = 0;
    
    //
    //  Load/create tiles, layout and layout the board.
    //  Note: in order to extend the code to cope with
    //  various layouts and tiles, this is the place to
    //  start working on.
    //
    bool ok;
    
    if (useTilesFromFile) {
        //
        // Open and load game from file
        //
        multimap<int, int> tileMap;
        if (!loadGameFromFile(tileMap)) return; // return map with id <-> index
        
        //
        // Re-order the tiles in the factory tile list according to what's in the file
        //
        multimap<int, int>::iterator iter;
        vector<smTileItem*>& theTiles = mTileFactory->tiles();
        vector<smTileItem*> theTilesCopy(theTiles);
        for (unsigned int k=0; k < theTilesCopy.size(); k++) {
            iter = tileMap.lower_bound(theTilesCopy[k]->id());
            if (iter == tileMap.end()) {
                QMessageBox::warning(0, "Solitaire Mahjong", "File does not contain a consistent set of tiles.\nCannot load game from file.\n");
                //
                // File is corrupt. Restore old list and continue.
                //
                theTiles = theTilesCopy;
                for (unsigned int j=0; j<theTiles.size(); j++) theTiles[j]->setIndex(j);
                break;
            }
            int ix = iter->second; // index
            theTiles[ix] = theTilesCopy[k];
            theTiles[ix]->setIndex(ix);
            theTiles[ix]->setHighlighted(false);
            tileMap.erase(iter);
        }
    }
    else {
        //
        // Generate tiles (random order)
        //
        delete mTileFactory; mTileFactory = 0;
        mTileFactory = new smTileFactory(mBoardView->scene());
        ok = mTileFactory->makeTiles(classic, static_cast<smTileSize>(smSettings::instance()->mCurrentTileSize));
    }
    
    delete mTileLayout; mTileLayout = 0;
    mTileLayout = new smTileLayout;
    ok = mTileLayout->makeLayout(turtle);
    QSize s = mTileLayout->layoutTiles(mTileFactory->tiles(),  // returns size needed to fit all tiles
                                       mTileFactory->shadowWidth(),
                                       mTileFactory->shadowHeight());
    // mTileLayout->asciiPrintLayout();  // layout debugging only
    
    //
    //  Resize everything so the whole board is visible.
    //
    update();
    mBoardView->scene()->setSceneRect(0, 0, s.width(), s.height());
    int borderWidth  = frameGeometry().width() - width();
    int borderHeight = frameGeometry().height() - height();  
    QSize fs(s.width()+borderWidth, s.height()+borderHeight);
    resize(fs);
    setFixedSize(fs);
    update();
    
    //
    //  Make all tiles visible
    //
    vector<smTileItem*> &theList = mTileFactory->tiles();
    for(unsigned int i=0; i<theList.size(); i++) theList[i]->setVisible(true);
    mBoardView->scene()->update();
    
    mCurrentNumberOfTiles = smMaxNumberOfTiles;
    mCurrentNumberOfUndos = 0;
    mCurrentNumberOfHints = 0;
    ui.actionHint->setEnabled(true);
    ui.actionUndo->setEnabled(false);
    ui.actionReplay->setEnabled(false);
    ui.actionFinish_Game->setEnabled(true);
    
    mGameIsOn = true;
    
    char text[100];
    statusBar()->clearMessage();  // remove temporary messages from menu bars etc.
    sprintf(text, "New game started (%d tiles)", mCurrentNumberOfTiles);
    mMessageLabel->setText(text);
    sprintf(text, " %d ",mCurrentNumberOfTiles);
    mTileCountLabel->setText(text);
    sprintf(text, " %d/%d ", mCurrentNumberOfHints, mCurrentNumberOfUndos);
    mUndoHintCountLabel->setText(text);
#if defined(Q_OS_MACX)
    setFocus();   // get focus back from the Mac top menu bar
#endif
    mStartTime = time(0);
}

void smMainWindow::undoMove()
{
#if defined(Q_OS_MACX)
    setFocus();   // get focus back from the Mac top menu bar
#endif
    if (!mGameIsOn || mHistoryStack.empty()) return;
    //
    //  Unhighlight selected items
    //
    if (mFirstSelected) mFirstSelected->setHighlighted(false);
    if (mSecondSelected) mSecondSelected->setHighlighted(false);
    mFirstSelected = mSecondSelected = 0;
    
    //
    // Make the first items on the history stack reappear
    //
    pair<smTileItem*, smTileItem*> p = mHistoryStack.top();
    mHistoryStack.pop();
    p.first->undoRemove();
    p.first->setHighlighted(false);
    mTileLayout->undoRemoveTile(p.first);
    p.second->undoRemove();
    p.second->setHighlighted(false);
    mTileLayout->undoRemoveTile(p.second);
    mCurrentNumberOfTiles += 2;
    
    if (mHistoryStack.empty()) {
        ui.actionReplay->setEnabled(false);
        ui.actionUndo->setEnabled(false);
    }
    else {
        ui.actionReplay->setEnabled(true);
        ui.actionUndo->setEnabled(true);
    }
    
    char text[64];
    statusBar()->clearMessage();
    sprintf(text, " %d ", mCurrentNumberOfTiles);
    mTileCountLabel->setText(text);
    
    mCurrentNumberOfUndos++;
    
    sprintf(text, " %d/%d ", mCurrentNumberOfHints, mCurrentNumberOfUndos);
    mUndoHintCountLabel->setText(text);
}







